Sources/apm-agent-ios/OpenTelementry Extensions/ElasticSpanProcessor.swift (116 lines of code) (raw):
// Copyright © 2023 Elasticsearch BV
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
import NetworkStatus
import OpenTelemetryApi
import OpenTelemetrySdk
import os.log
public struct ElasticSpanProcessor: SpanProcessor {
var processor: SpanProcessor
var exporter: SpanExporter
var filters = [SignalFilter<ReadableSpan>]()
public let isStartRequired: Bool
public let isEndRequired: Bool
#if os(iOS) && !targetEnvironment(macCatalyst)
static var netstatInjector: NetworkStatusInjector? = { () -> NetworkStatusInjector? in
do {
let netstats = try NetworkStatus()
return NetworkStatusInjector(netstat: netstats)
} catch {
if #available(iOS 14, macOS 11, tvOS 14, *) {
os_log(
.error, "failed to initialize network connection status: %@", error.localizedDescription)
} else {
NSLog("failed to initialize network connection status: %@", error.localizedDescription)
}
return nil
}
}()
#endif // os(iOS) && !targetEnvironment(macCatalyst)
public init(
spanExporter: SpanExporter,
_ filters: [SignalFilter<ReadableSpan>] = [SignalFilter<ReadableSpan>](),
scheduleDelay: TimeInterval = 5, exportTimeout: TimeInterval = 30,
maxQueueSize: Int = 2048, maxExportBatchSize: Int = 512,
willExportCallback: ((inout [SpanData]) -> Void)? = nil
) {
processor = BatchSpanProcessor(
spanExporter: spanExporter, scheduleDelay: scheduleDelay, exportTimeout: exportTimeout,
maxQueueSize: maxQueueSize, maxExportBatchSize: maxExportBatchSize,
willExportCallback: willExportCallback)
isStartRequired = processor.isStartRequired
isEndRequired = processor.isEndRequired
exporter = spanExporter
self.filters = filters
}
public func onStart(
parentContext: OpenTelemetryApi.SpanContext?, span: OpenTelemetrySdk.ReadableSpan
) {
span.setAttribute(
key: ElasticAttributes.sessionId.rawValue,
value: AttributeValue.string(SessionManager.instance.session()))
#if os(iOS) && !targetEnvironment(macCatalyst)
if let networkStatusInjector = Self.netstatInjector {
networkStatusInjector.inject(span: span)
}
#endif // os(iOS) && !targetEnvironment(macCatalyst)
span.setAttribute(key: "type", value: AttributeValue.string("mobile"))
processor.onStart(parentContext: parentContext, span: span)
}
public mutating func onEnd(span: OpenTelemetrySdk.ReadableSpan) {
for filter in filters where !filter.shouldInclude(span) {
return
}
if span.isHttpSpan() {
var spanData = span.toSpanData()
if spanData.parentSpanId == nil, let transactionSpan = span as? RecordEventsReadableSpan {
var newAttributes = AttributesDictionary(capacity: spanData.attributes.count)
newAttributes.updateValue(value: AttributeValue.string("mobile"), forKey: "type")
newAttributes.updateValue(
value: AttributeValue.string(SessionManager.instance.session()),
forKey: ElasticAttributes.sessionId.rawValue)
let parentSpanContext = SpanContext.create(
traceId: span.context.traceId, spanId: SpanId.random(), traceFlags: TraceFlags(),
traceState: TraceState())
let parentSpan = RecordEventsReadableSpan.startSpan(
context: parentSpanContext,
name: spanData.name,
instrumentationScopeInfo: span.instrumentationScopeInfo,
kind: span.kind,
parentContext: nil,
hasRemoteParent: false,
spanLimits: transactionSpan.spanLimits,
spanProcessor: NoopSpanProcessor(),
clock: transactionSpan.clock,
resource: transactionSpan.resource,
attributes: newAttributes,
links: transactionSpan.links,
totalRecordedLinks: transactionSpan.totalRecordedLinks,
startTime: transactionSpan.startTime)
parentSpan.end(time: transactionSpan.endTime!)
spanData.settingParentSpanId(parentSpanContext.spanId)
_ = exporter.export(spans: [spanData, parentSpan.toSpanData()])
return
}
} else {
#if os(iOS) && !targetEnvironment(macCatalyst)
span.setAttribute(key: SemanticAttributes.networkConnectionType.rawValue,
value: AttributeValue.string(NetworkStatusManager().status()))
#endif // os(iOS) && !targetEnvironment(macCatalyst)
}
processor.onEnd(span: span)
}
public mutating func shutdown(explicitTimeout: TimeInterval? = nil) {
processor.shutdown(explicitTimeout: explicitTimeout)
}
public func forceFlush(timeout: TimeInterval?) {
processor.forceFlush(timeout: timeout)
}
}
internal struct NoopSpanProcessor: SpanProcessor {
init() {}
let isStartRequired = false
let isEndRequired = false
func onStart(parentContext: SpanContext?, span: ReadableSpan) {}
func onEnd(span: ReadableSpan) {}
func shutdown(explicitTimeout: TimeInterval? = nil) {}
func forceFlush(timeout: TimeInterval? = nil) {}
}